/*
 * cpu_arm.c: CPU driver for arm CPUs
 *
 * Copyright (C) 2013 Red Hat, Inc.
 * Copyright (C) Canonical Ltd. 2012
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library.  If not, see
 * <http://www.gnu.org/licenses/>.
 */

#include <config.h>

#if defined(__aarch64__)
#include <asm/hwcap.h>
#include <sys/auxv.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#endif

#include "viralloc.h"
#include "cpu.h"
#include "virstring.h"

#define VIR_FROM_THIS VIR_FROM_CPU

static const virArch archs[] = {
    VIR_ARCH_ARMV6L,
    VIR_ARCH_ARMV7B,
    VIR_ARCH_ARMV7L,
    VIR_ARCH_AARCH64,
};


static int
virCPUarmUpdate(virCPUDefPtr guest,
                const virCPUDef *host)
{
    int ret = -1;
    virCPUDefPtr updated = NULL;

    if (guest->mode != VIR_CPU_MODE_HOST_MODEL)
        return 0;

    if (!host) {
        virReportError(VIR_ERR_CONFIG_UNSUPPORTED, "%s",
                       _("unknown host CPU model"));
        goto cleanup;
    }

    if (!(updated = virCPUDefCopyWithoutModel(guest)))
        goto cleanup;

    updated->mode = VIR_CPU_MODE_CUSTOM;
    if (virCPUDefCopyModel(updated, host, true) < 0)
        goto cleanup;

    virCPUDefStealModel(guest, updated, false);
    guest->mode = VIR_CPU_MODE_CUSTOM;
    guest->match = VIR_CPU_MATCH_EXACT;
    ret = 0;

 cleanup:
    virCPUDefFree(updated);
    return ret;
}


static virCPUDefPtr
virCPUarmBaseline(virCPUDefPtr *cpus,
                  unsigned int ncpus ATTRIBUTE_UNUSED,
                  virDomainCapsCPUModelsPtr models ATTRIBUTE_UNUSED,
                  const char **features ATTRIBUTE_UNUSED,
                  bool migratable ATTRIBUTE_UNUSED)
{
    virCPUDefPtr cpu = NULL;

    if (VIR_ALLOC(cpu) < 0 ||
        VIR_STRDUP(cpu->model, cpus[0]->model) < 0) {
        virCPUDefFree(cpu);
        return NULL;
    }

    cpu->type = VIR_CPU_TYPE_GUEST;
    cpu->match = VIR_CPU_MATCH_EXACT;

    return cpu;
}

static virCPUCompareResult
virCPUarmCompare(virCPUDefPtr host ATTRIBUTE_UNUSED,
                 virCPUDefPtr cpu ATTRIBUTE_UNUSED,
                 bool failMessages ATTRIBUTE_UNUSED)
{
    return VIR_CPU_COMPARE_IDENTICAL;
}

#if defined(__aarch64__)
#define MAX_CPU_FLAGS			32
#define BIT_SHIFTS(n)			(1UL << (n))

/* Generate human readable flag list according to the order of AT_HWCAP bit map */
const char *aarch64_cpu_flags[MAX_CPU_FLAGS] = {
	"fp", "asimd", "evtstrm", "aes", "pmull", "sha1", "sha2",
	"crc32", "atomics", "fphp", "asimdhp", "cpuid", "asimdrdm",
	"jscvt", "fcma", "lrcpc", "dcpop", "sha3", "sm3", "sm4",
	"asimddp", "sha512", "sve", "asimdfhm", "dit", "uscat",
	"ilrcpc", "flagm", "ssbs", "sb", "paca", "pacg"
};

static inline int dmi_read_data(int fd, char *buf, int count)
{
	int len = 0;

	do {
		len = read(fd, buf, count);
	} while (len == -1 && errno == EINTR);

	if (len < 0)
		return -1;

	return len;
}

static char *dmi_get_string(char *buf, uint8_t skip, int *len)
{
	char *bp = buf;

	if (skip == 0)
		return NULL;

	while (skip > 1 && *bp) {
		bp += strlen(bp);
		bp++;
		skip--;
	}

	if (!*bp)
		return NULL;

	*len = (int)strlen(bp);

	return bp;
}

static int dmi_get_model_and_vendor(char **model, char **vendor)
{
	int fd;
	int ret = 0, len = 0;
	char buf[128], *bp = buf, *str = NULL;
#define DMI_MFR_OFFSET			0x07
#define DMI_VERSION_OFFSET		0x10

	fd = open("/sys/firmware/dmi/entries/4-0/raw", O_RDONLY);
	if (fd < 0)
		return -1;

	ret = dmi_read_data(fd, buf, 128);
	if (ret < 0)
		return -1;

	close(fd);

	/* skip dmi header and pointer to data */
	bp += buf[1];

	/* model info */
	str = dmi_get_string(bp, buf[DMI_MFR_OFFSET], &len);
	if (str)
		*vendor = strndup(str, len);

	/* vendor info */
	str = dmi_get_string(bp, buf[DMI_VERSION_OFFSET], &len);
	if (str)
		*model = strndup(str, len);

	if (*model == NULL || *vendor == NULL)
		return -1;

#undef DMI_MFR_OFFSET
#undef DMI_VERSION_OFFSET

	return 0;
}

static int virCPUarmGetHost(virCPUDefPtr cpu, virDomainCapsCPUModelsPtr models)
{
	unsigned long hwcaps;
	int i, idx = 0;

	/* unused */
	(void)models;

	if (dmi_get_model_and_vendor(&cpu->model, &cpu->vendor) < 0) {
		virReportError(VIR_ERR_INTERNAL_ERROR, "%s", _("DMI info failed"));
		return -1;
	}

	if (!(getauxval(AT_HWCAP) & HWCAP_CPUID)) {
		virReportError(VIR_ERR_INTERNAL_ERROR, "%s", _("CPUID registers unavailable"));
		return -1;
	}

	hwcaps = getauxval(AT_HWCAP);
	if (!hwcaps) {
		virReportError(VIR_ERR_INTERNAL_ERROR, "%s", _("AT_HWCAP is not found"));
		return -1;
	}

	/* count total supported-features. */
	for (i = 0; i < MAX_CPU_FLAGS; i++) {
		if (hwcaps & BIT_SHIFTS(i))
			cpu->nfeatures++;
	}

	if (VIR_ALLOC_N(cpu->features, cpu->nfeatures) < 0)
		return -1;

	/* match cpu feature */
	for (i = 0; i < MAX_CPU_FLAGS; i++) {
		if (hwcaps & BIT_SHIFTS(i)) {
			cpu->features[idx].policy = VIR_CPU_FEATURE_REQUIRE;
			cpu->features[idx].name = strdup(aarch64_cpu_flags[i]);
			idx++;
		}
	}

	return 0;
}
#endif

struct cpuArchDriver cpuDriverArm = {
    .name = "arm",
    .arch = archs,
    .narch = ARRAY_CARDINALITY(archs),
    .compare = virCPUarmCompare,
#if defined(__aarch64__)
	.getHost = virCPUarmGetHost,
#endif
    .decode = NULL,
    .encode = NULL,
    .baseline = virCPUarmBaseline,
    .update = virCPUarmUpdate,
};
